]> dgit.raspbian.org Git - puma.git/commitdiff
[PATCH] 7.2.1 backport (#3947)
authorNate Berkopec <nate.berkopec@gmail.com>
Tue, 26 May 2026 23:33:21 +0000 (08:33 +0900)
committerAbhijith PA <abhijith@debian.org>
Mon, 5 Oct 2026 04:40:12 +0000 (10:10 +0530)
* Limit and anchor PROXY protocol parsing

(cherry picked from commit f1f37d5822aa8ffe23e7d0d16e9a1f3b0eaf9bfd)

* Parse PROXY protocol only once per connection

(cherry picked from commit 8310c17cd43784723ed41a94d2a747da256306de)

Gbp-Pq: Name CVE-2026-47736_37.patch

lib/puma/client.rb
lib/puma/const.rb
test/test_puma_server.rb

index b5a1569c6869eeab7756261eb0a4769534645008..55d7982fe2ceb634ff9c2eecf597847424f1e77b 100644 (file)
@@ -148,7 +148,7 @@ module Puma
     def reset(fast_check=true)
       @parser.reset
       @read_header = true
-      @read_proxy = !!@expect_proxy_proto
+      @read_proxy = !!@expect_proxy_proto && @requests_served.zero?
       @env = @proto_env.dup
       @body = nil
       @tempfile = nil
@@ -196,20 +196,36 @@ module Puma
     def try_to_parse_proxy_protocol
       if @read_proxy
         if @expect_proxy_proto == :v1
-          if @buffer.include? "\r\n"
-            if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
-              if md[1]
-                @peerip = md[1].split(" ")[0]
+          crlf_index = @buffer.index "\r\n"
+
+          unless crlf_index
+            if "PROXY ".start_with? @buffer
+              return false
+            elsif @buffer.start_with? "PROXY "
+              if @buffer.bytesize >= PROXY_PROTOCOL_V1_MAX_LENGTH
+                raise ConnectionError, "PROXY protocol v1 line is too long"
               end
-              @buffer = md.post_match
+              return false
             end
-            # if the buffer has a \r\n but doesn't have a PROXY protocol
-            # request, this is just HTTP from a non-PROXY client; move on
+
             @read_proxy = false
-            return @buffer.size > 0
-          else
-            return false
+            return true
+          end
+
+          if @buffer.start_with?("PROXY ") && crlf_index + 2 > PROXY_PROTOCOL_V1_MAX_LENGTH
+            raise ConnectionError, "PROXY protocol v1 line is too long"
+          end
+
+          if md = PROXY_PROTOCOL_V1_REGEX.match(@buffer)
+            if md[1]
+              @peerip = md[1].split(" ")[0]
+            end
+            @buffer = md.post_match
           end
+          # if the buffer has a \r\n but doesn't have a PROXY protocol
+          # request, this is just HTTP from a non-PROXY client; move on
+          @read_proxy = false
+          return @buffer.size > 0
         end
       end
       true
index 12b8bfd59370cf8c563a9346c666f19c677b75af..048ed61a5c1aee68c1c600f1cdc0bafceb0cca69 100644 (file)
@@ -255,6 +255,7 @@ module Puma
     # Banned keys of response header
     BANNED_HEADER_KEY = /\A(rack\.|status\z)/.freeze
 
-    PROXY_PROTOCOL_V1_REGEX = /^PROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
+    PROXY_PROTOCOL_V1_REGEX = /\APROXY (?:TCP4|TCP6|UNKNOWN) ([^\r]+)\r\n/.freeze
+    PROXY_PROTOCOL_V1_MAX_LENGTH = 107
   end
 end
index 81bf23e7144d1c4530ecc04e103bcba9a0003e30..52a45c2312dfca0c134babbae9322c21123965a6 100644 (file)
@@ -1065,6 +1065,75 @@ EOF
     assert_equal 'fd00::1', remote_addr
   end
 
+  def test_proxy_protocol_rejects_line_without_crlf_at_max_length
+    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do
+      [200, {}, ["Hello"]]
+    end
+
+    socket = new_connection
+    socket << "PROXY #{'A' * (Puma::Const::PROXY_PROTOCOL_V1_MAX_LENGTH - "PROXY ".bytesize)}"
+
+    assert_equal "", socket.read
+  end
+
+  def test_proxy_protocol_allows_non_proxy_requests_over_max_length
+    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do
+      [200, {}, ["Hello"]]
+    end
+
+    response = send_http_and_read( "GET /#{'a' * Puma::Const::PROXY_PROTOCOL_V1_MAX_LENGTH} HTTP/1.0\r\n\r\n")
+
+    assert_match %r{\AHTTP/1\.[01] 200 OK}, response
+    assert_match %r{Hello\z}, response
+  end
+
+  def test_proxy_protocol_ignores_embedded_proxy_line_in_http_body
+    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env|
+      body = env['rack.input'].read
+      [200, {'Content-Length' => body.bytesize.to_s}, [body]]
+    end
+
+    body = "prefix\nPROXY TCP4 1.1.1.1 2.2.2.2 3 4\r\nsuffix"
+    response = send_http_and_read ("POST / HTTP/1.1\r\nHost: test.com\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}")
+
+    assert_match %r{\AHTTP/1\.[01] 200 OK}, response
+    assert_match %r{#{Regexp.escape(body)}\z}, response
+  end
+
+  def test_proxy_protocol_only_parses_first_request_on_connection
+    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1) do |env|
+      [200, {}, [env["REMOTE_ADDR"]]]
+    end
+
+    socket = send_proxy_v1_http("GET /one HTTP/1.1\r\nHost: test.com\r\n\r\n", "1.2.3.4")
+
+    assert_equal ["HTTP/1.1 200 OK", "Content-Length: 7"], header(socket)
+    assert_equal "1.2.3.4", socket.read(7)
+
+    sleep 1
+
+    socket << "PROXY TCP4 127.0.0.1 127.0.0.1 10000 80\r\nGET /two HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n"
+
+    assert_match %r{\AHTTP/1\.[01] 400 Bad Request}, socket.read
+  end
+
+  def test_proxy_protocol_allows_proxy_http_method_on_keep_alive
+    server_run(remote_address: :proxy_protocol, remote_address_proxy_protocol: :v1, supported_http_methods: :any) do |env|
+      [200, {}, [env["REMOTE_ADDR"]]]
+    end
+
+    socket = send_proxy_v1_http("GET /one HTTP/1.1\r\nHost: test.com\r\n\r\n", "1.2.3.4")
+
+    assert_equal ["HTTP/1.1 200 OK", "Content-Length: 7"], header(socket)
+    assert_equal "1.2.3.4", socket.read(7)
+
+    socket << "PROXY /two HTTP/1.1\r\nHost: test.com\r\nConnection: close\r\n\r\n"
+
+    response = socket.read
+    assert_match %r{\AHTTP/1\.1 200 OK\r\n}, response
+    assert_equal "1.2.3.4", response.split("\r\n").last
+  end
+
   # To comply with the Rack spec, we have to split header field values
   # containing newlines into multiple headers.
   def assert_does_not_allow_http_injection(app, opts = {})